/*
 * GlucoseServer.java
 *
 * Created on March 17, 2006, 10:27 AM
 *
 * To change this template, choose Tools | Options and locate the template under
 * the Source Creation and Management node. Right-click the template and choose
 * Open. You can then make changes to the template in the Source Editor.
 */

package Comm.Glucose;

import Comm.Util.Exceptions.GlucoseException;
import Comm.Util.GarbageCharTask;
import Experience.ExpUI;
import Comm.Util.Logger;
import javax.bluetooth.*;
import javax.microedition.io.*;
import java.io.*;
import java.util.Timer;
import java.util.Vector;

/**
 *
 * @author dmgreen
 */
public class UltraServer implements Runnable {
    
    public final static UUID spp_uuid = new UUID("1101", false);
    public StreamConnectionNotifier server;
    
    private final static int SERVICE_TELEPHONY = 0x400000;
    
    private boolean DONE = false;
    
    // Bluetooth singleton object
    LocalDevice device;
    DiscoveryAgent agent;
    
    StreamConnection c = null;
    
    Timer myTimeoutTimer = new Timer();
    
    // am i in the middle of a timeout/blackout period
    boolean myTimeoutFlag = false;
    
    /** Creates a new instance of GlucoseServer */
    public UltraServer() {
         
    }
    
    // NEED TO PARAMETERIZE THIS!
    public void log(String msg, String msg_mode) {
        Logger.getLog().addLogMessage(msg, msg_mode);
    }
    
    public void setTimeoutFlag(boolean status) {
        myTimeoutFlag = status;
        
        // you DO want to do this in either scenario
        if (myTimeoutTimer != null)  {
            myTimeoutTimer.cancel();
            myTimeoutTimer = null;
            myTimeoutTimer = new Timer();
        }
    }
    
    private Integer parseGlucoLine(String gluco_data) throws Exception {
        int comma_idx = gluco_data.indexOf(",");
        if (comma_idx < 0) {
            return null;
        }
        
        int next_comma_idx = gluco_data.indexOf(",", comma_idx+1);
        if (next_comma_idx < 0) {
            return null;
        }
        
        String reading = gluco_data.substring(comma_idx+1
                ,next_comma_idx);
        //System.out.println("1. READING IS \"" + reading + "\"");
        reading = reading.replace('\"', ' ');
        //System.out.println("2. READING IS \"" + reading + "\"");
        if ( ((String) reading.trim()).equals("")) {
            return null;
        }
        else {
            try {
                return Integer.valueOf(reading.trim());
            } catch (Exception e) {
                return null;
            }
        }
    }
    
    public void start_server() {
        try
        {
            //
            // initialize the JABWT stack
            device = LocalDevice.getLocalDevice(); // obtain reference to singleton
            device.setDiscoverable(DiscoveryAgent.GIAC); // set Discover mode to LIAC
      
             // start a thread to serve the server connection.
            // for simplicity of this demo, we only start one server thread
            // see run() for the task of this thread
            DONE = false;
            Thread t = new Thread( this );
            t.start();

        } catch ( BluetoothStateException e )
        {
            e.printStackTrace();
        }
    }
    
    
    public void stop_server() {
        log("Stopping server ...", Logger.MSG_INFO);
        if (server != null) {
            try {
                DONE = true;
                try {
                    server.close();
                } catch (Exception e) {
                    log("Unexpected exception in here ...", Logger.MSG_ERROR);
                }
                log("Successfully closed server :)", Logger.MSG_DEBUG);
                server = null;
            } catch (Exception e) {
                log("Error while closing server :( " + e.toString(), Logger.MSG_ERROR);
            }
        }
    }
    
    public void toggleServer() {
        if (server == null) {
            start_server();
        }
        else{
            stop_server();
        }
        
    }
    
    public void add_readings_to_sample(String[] readings) {
        
    }
    
    public void send_kill_char(OutputStream out) throws IOException {
        out.write(4);
        out.flush();
        /* log("Successfully wrote kill char", Logger.MSG_DEBUG); */
    }
    
    public void send_query(OutputStream out) throws IOException {
       String query_string = "DMP";
       byte[] query_bytes = query_string.getBytes();
        
       log ("trying to write \"DMP\" query", Logger.MSG_DEBUG);
       out.write('D');
       out.flush();
       
       doWait(100);
       
       out.write('M');
       out.flush();
       
       doWait(100);
       
       out.write('P');
       out.flush();
       
       doWait(100);
       log("Successfully wrote \"DMP\" with 3 calls to \"write\"", Logger.MSG_DEBUG);

       /* was 2 seconds ... will this still work?*/
       doWait(1000);
       
       
       // need to check and see if you get the right result here ...
       
       // what's the ASCII code for yen?
       // how will you decide when to stop reading? will you actually parse the header line and
       // read a certain number of characters based on that?
       
       
       /* What works and what doesn't
        * ---------------------------
        * Writing all the characters in a byte array DOES NOT WORK
        * Writing each character individually, in (immediate) serial DOES NOT WORK
        * Writing each character with a flush after DOES NOT WORK
        * Writing each character with a flush and a pause (twice?)WORKS <<<<<<<<<<<<<<<<<<<<<<<<< !!
        *
        */
       
    }
    
    /*
    public void send_query2(OutputStream out) throws IOException {
        String query_string = "DM?";
        byte[] query_bytes = query_string.getBytes();
    
       // this one did not appear to work in hyperterminal
       out.writeUTF(query_string);
       out.flush();
       log("Successfully wrote \"DM?\" with \"writeUTF\"");
    }
     **/
    
    /*
    public void send_query3(OutputStream out) throws IOException {
        String query_string = "DM?";
        byte[] query_bytes = query_string.getBytes();

       // this one appears to work in hyperterminal
       out.writeChars(query_string);
       out.flush();
       log("Successfully wrote \"DM?\" with \"writeChars\"");  
    }
     */
    
    /*
    public void send_query4(OutputStream out) throws IOException {
        String query_string = "DM?";
        byte[] query_bytes = query_string.getBytes();
        
        for (int i=0; i< query_bytes.length; i++) {
            out.writeByte(query_bytes[i]);
            //dout.write(query_bytes[i]);
        }
        out.flush();
        log("Successfully wrote \"DM?\" with \"writeByte loop\"");
    }
      
     */
     
    /*
    public void send_query5(OutputStream out) throws IOException {
        String query_string = "DM?";
        byte[] query_bytes = query_string.getBytes();
       
        out.writeChar(68);
        out.writeChar(77);
        out.writeChar(63);
        out.flush();
        log("Successfully write \"DM?\" as individual chars");  
    }
     */
    
    public String[] read_response(InputStream in) throws IOException {
        log ("Reading query response", Logger.MSG_DEBUG);
        
        if (c == null) {
            log ("Connection is null!", Logger.MSG_DEBUG);
            return null;
        }
        
        byte[] byte_buffer = new byte[20];
        int num_bytes_read;
        
        Vector line_vec = new Vector();
        
        StringBuffer response_buf = new StringBuffer();
               
        boolean keep_on_going   = true;
        int line_feed_count = 0;
                          
        // log("in.available() is " + in.available(), Logger.MSG_DEBUG);
        
        if (in.available() == 0) {
            log("No data available on InputStream", Logger.MSG_DEBUG);
            return null;
        }
        
        while ( (in.available() > 0) && (line_feed_count < 3) )   {
            /*log ("Attempting to read ...", Logger.MSG_DEBUG); */
            // what if you don't get any response?!? this will hang :(
            num_bytes_read = in.read(byte_buffer);
            String read_string = new String(byte_buffer); 
            String add_string = "";
            
            log ("Read " + num_bytes_read + " bytes: " + read_string, Logger.MSG_DEBUG);

            int yen_index = read_string.indexOf(165);
            if ( yen_index >= 0) {
                log("got a yen character", Logger.MSG_DEBUG);
                
                /* this doesn't work; but because this character has been filtered out on the
                 * glucometer side, this code never gets called
                 */
                if (read_string.equals(String.valueOf(65))) {
                    log ("... with nothing else", Logger.MSG_DEBUG);
                    return null;
                }
                else {
                    // there is other stuff in there as well
                    log ("... with other data", Logger.MSG_DEBUG);
                    add_string = read_string.substring(0,yen_index) + 
                                read_string.substring(yen_index+1,num_bytes_read);
                }
            } else {
                add_string = read_string.substring(0,num_bytes_read);
            }
            
            //response_buf.append( new String(byte_buffer).substring(0,num_bytes_read) );
            response_buf.append(add_string);
            
                
            if (read_string.indexOf(10) >= 0) {
                line_feed_count++;
                log ("Found a line feed. Now have : " + line_feed_count, Logger.MSG_DEBUG);
            }
            
            try {
                Thread.sleep(500);
            } catch (InterruptedException ie) {}
        }
        
                
        String buf_as_string = response_buf.toString();
        
        while (buf_as_string.indexOf("\r\n") >= 0) {
            int nln_loc = buf_as_string.indexOf("\r\n");
             //log("Found a newline at " + nln_loc);
            String data_line = buf_as_string.substring(0,nln_loc);
            //log("Line is " + data_line);
            line_vec.addElement(data_line);
            
            response_buf.delete(0,nln_loc+2);
            buf_as_string = response_buf.toString();
         }
    
        log("Total number of lines read is " + line_vec.size(), Logger.MSG_DEBUG);
        
        String[] response_lines = new String[line_vec.size()];
        line_vec.copyInto(response_lines);
        
        return response_lines;
    }
    
    public int parse_samples(String[] lines) throws GlucoseException, IndexOutOfBoundsException {
        
        /* see if you have any lines at all */
        if (lines.length == 0) {
            log ("Found 0 lines of data; returning", Logger.MSG_DEBUG);
            return -1;
        }
        
        /* try to parse the header (first) line to see how many samples there are */
        String header = lines[0];
        if(header.indexOf("P ") != 0) {
            throw new GlucoseException("Couldn't find \"P_\" in header");
        }

        /* make sure that the num samples listed in the header matches the number of lines
         * actually parsed */
        String num_samples_str = header.substring(2,5);
        int num_samples;
        try {
            num_samples = Integer.parseInt(num_samples_str);
        } catch (NumberFormatException nfe) {
            throw new GlucoseException("Couldn't parse number of entries.");
        }    
        
        log ("Number of samples stored on glucometer is: " + num_samples, Logger.MSG_INFO);
        
        /* Don't do this check ....
        if (lines.length-1 != num_samples) {
            throw new GlucoseException("Number of lines in header doesn't match actual.");
        }
        */
    
        if (num_samples == 0) {
            throw new GlucoseException("Found zero samples!");
        }
    
        /* Get the glucose reading from the last sample (line) */
        //String last_sample_data = lines[lines.length-1];
        // though this doesn't seem to work!
        String last_sample_data = lines[1];
        
        if(last_sample_data.indexOf("P ") != 0) {
            throw new GlucoseException("Couldn't find \"P_\" in sample");
        }

        String last_sample_reading = last_sample_data.substring(34,40);
        last_sample_reading = last_sample_reading.trim();
        if (last_sample_reading.length() > 3) {
            log("Disregarding sample - is it a control?: " + last_sample_reading, Logger.MSG_INFO);
            return -1;
        }
        else {
            int got_a_reading = -1;
            try {
                got_a_reading = Integer.parseInt(last_sample_reading);
            } catch (NumberFormatException nfe) { 
                log ("Error converting glucose sample '" + last_sample_reading + "' to int!", Logger.MSG_ERROR);
            }
            log(">>>Got for sample: " + last_sample_reading + "<<<", Logger.MSG_INFO);
            return got_a_reading;
        }
        
    }
    
    private String[] doQueryResponse(OutputStream out, InputStream in) throws IOException {
        send_query(out);
        doWait(1000);
        String[] response_lines = read_response(in);
        doWait(1000);
        
        return response_lines;
    }
    
    private void doWait(long wait_milli) {
        try { 
            Thread.sleep(wait_milli);
        } catch (InterruptedException ie) {
            log("Got an interrupted exception ?!?", Logger.MSG_ERROR);
        }
    }
    
    
    /*
     * This function can throw IOExceptions in two ways:
     * 1) If the code is blocked on the 'read' call and the 
     * the bluetooth connection is abruptly pulled out from under it
     * 2) If the bluetooth connection is closed properly from the other side
     * and a call to read returns the EOF character.
     *
     */
    public void doReadLoop(InputStream in) throws IOException {
        byte[] dummy = new byte[30];
        // eat up the rest of the data ...
        log ("Entering waiting loop", Logger.MSG_DEBUG);
        int dummy_bytes_read=0;
        
        while (true) {
            dummy_bytes_read = in.read(dummy); 
            if (dummy_bytes_read == -1) {
                log ("EOF reached ... throwing IO Exception", Logger.MSG_DEBUG);
                throw new IOException();
            }
            doWait(500);                        
        }
    }
    
    public void doCloseStreams(InputStream in, OutputStream out, StreamConnection c) {
        try {
            if (in != null) {
                in.close();
                in = null;
            }
            if (out != null) {
                out.close(); 
                out=null;
            }
            if (c != null) {
                c.close();
                c=null;
            }
        } catch (IOException ioe) {
            log("Error closing streams: " + ioe.getMessage(), Logger.MSG_ERROR);
        }
    }
    
    public void run() {
        String appName = "GlucoseServer";


        // connection to remote device
        try
        {
          String url = "btspp://localhost:" + spp_uuid.toString() +";authenticate=false;encrypt=false;name="+ appName;
          server =  (StreamConnectionNotifier)Connector.open( url );
          // Retrieve the service record template
          ServiceRecord rec = device.getRecord( server );

          // set ServiceRecrod ServiceAvailability (0x0008) attribute to indicate our service is available
          // 0xFF indicate fully available status
          // This operation is optional
          rec.setAttributeValue( 0x0008, new DataElement( DataElement.U_INT_1, 0xFF ) );

          // Set the Major Service Classes flag in Bluetooth stack.
          // We choose Telephony Service
          rec.setDeviceServiceClasses( SERVICE_TELEPHONY  );



         } catch (Exception e) {
              e.printStackTrace();
          }

        while(!DONE)
        {
            try {
                log("UltraServer waiting for a connection", Logger.MSG_DEBUG);
                c = server.acceptAndOpen();
                doWait(1000);
                log("Found a connection", Logger.MSG_DEBUG);
                RemoteDevice rdev = RemoteDevice.getRemoteDevice( c );
                doWait(1000);
            }
            catch (IOException ioe) {
                log("GOT an IOException while establishing bluetooth connection", Logger.MSG_ERROR);
                /* this means that the connection was lost while the server was blocking on open.
                 WHY would this happen? When I manually kill the server? */
                
                continue;
            }    
            

            String[] response_lines  = null;
            int the_reading = -1;
            
            OutputStream out = null;
            InputStream  in  = null;
            
            try {
                
                out = c.openOutputStream();
                if(myTimeoutFlag) {
                    log("Sending kill characters", Logger.MSG_DEBUG);
                    send_kill_char(out);
                    send_kill_char(out);
                    send_kill_char(out);
                    
                    doCloseStreams(in,out,c);
                    doWait(1000);
                    continue;
                }
                
                in  = c.openInputStream();
               
               int attempt_num = 1;
               while ( (attempt_num < 4) && (response_lines == null) )  {
                   log ("Query attempt: #" + attempt_num, Logger.MSG_DEBUG);
                   attempt_num++;
                   response_lines = doQueryResponse(out,in);
               }
                   
               if (response_lines == null) {
                    log("Response lines is null; no data received from glucometer", Logger.MSG_ERROR);
               }
               
               // start the timeout process
               this.setTimeoutFlag(true);
               myTimeoutTimer.schedule(new GarbageCharTask(this), 150000);
               log ("Scheduling a 150 second timeout", Logger.MSG_DEBUG);
               
               if (response_lines != null) {
                   for (int i=0; i< response_lines.length; i++) {
                        log("Response line: " + response_lines[i], Logger.MSG_DEBUG);
                   }
                   
                   try {
                       the_reading = parse_samples(response_lines);
                   } catch (GlucoseException ge) {
                       log ("Got a GlucoseException while parsing samples: " + ge.getMessage(), Logger.MSG_ERROR);
                   } catch (IndexOutOfBoundsException ioobe) {
                       log ("Got a IndexOutOfBoundsException while parsing samples: " + ioobe.getMessage(), Logger.MSG_ERROR);
                   } catch (Exception e) {
                       log ("Got an ??? Exception while parsing samples:" + e.getMessage(), Logger.MSG_ERROR);
                   }
               } 
                
            } catch (IOException ioe) {
                log ("Got an error while communicating with glucometer: " + ioe.getMessage(), Logger.MSG_ERROR);
                doCloseStreams(in,out,c);
                continue;
            } 
                       
           try {
               log("Sending kill characters", Logger.MSG_DEBUG);
               send_kill_char(out);
               send_kill_char(out);
               send_kill_char(out);
           } catch (IOException ioe_dontcare) {log("Error while sending kill chars; ignoring", Logger.MSG_DEBUG);}
           
            
            try {
                doReadLoop(in);        
            } catch (IOException ioe) {
                   log("IOException in doReadLoop - OK", Logger.MSG_DEBUG);
                  // means that the connection was cut off.
            } finally {
                doCloseStreams(in,out,c);
            }
                  
            log("Start ringing", Logger.MSG_DEBUG);
            // Brrrrrring Brrrrrrrring !!
            ExpUI.getMIDlet().startRinging(); 
            ExpUI.getMIDlet().startNewSample();
            if (the_reading != -1) {
                ExpUI.MY_MIDLET.addGlucoseToSample(the_reading);
            }
        } // /WHILE
        log("Exited while loop; thread joining ... ", Logger.MSG_DEBUG);
    
    } // /FUNCTION 'run'

} // /CLASS
